// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ResharperSettingsManager.cs" company="Roman '4ux-nbIx' Novitsky">
//   Copyright (c) Roman '4ux-nbIx' Novitsky 2009
// </copyright>
// <summary>
//   Defines the SettingsManager type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------


namespace SettingsManager
{
  #region Namespace Imports

  using System;
  using System.Collections.Generic;
  using System.IO;
  using System.Linq;
  using System.Threading;
  using System.Xml;

  using ComponentSettingsManagers;

  using JetBrains.Annotations;
  using JetBrains.Application;
  using JetBrains.ComponentModel;
  using JetBrains.ProjectModel;
  using JetBrains.ProjectModel.Impl;
  using JetBrains.ReSharper.Daemon;
  using JetBrains.Util;

  using Properties;

  using HighlightingSettingsManager = ComponentSettingsManagers.HighlightingSettingsManager;

  #endregion


  /// <summary>
  /// Describes and manipulates the <c>ReSharper</c> settings manager.
  /// </summary>
  [ShellComponentInterface(ProgramConfigurations.VS_ADDIN)]
  [ShellComponentImplementation]
  [UsedImplicitly(ImplicitUseKindFlags.Default, ImplicitUseTargetFlags.WithMembers)]
  public sealed class ResharperSettingsManager : IXmlExternalizableShellComponent
  {
    #region Constants and Fields

    private readonly List<IComponentSettingsManager> _componentSettingsManagers = new List<IComponentSettingsManager>();
    private readonly FileSystemWatcher _fileSystemWatcher = new FileSystemWatcher();
    private readonly List<SettingsFile> _settingsFiles = new List<SettingsFile>();
    private string _settingsFolderPath;
    private int _settingsAutoUpdateInterval;

    #endregion


    #region Constructors and Destructors

    /// <summary>
    /// Initializes a new instance of the <see cref="ResharperSettingsManager"/> class.
    /// </summary>
    public ResharperSettingsManager()
    {
      _fileSystemWatcher.Changed += FileSystemChanged;
      _fileSystemWatcher.Filter = Resources.SettingsFileExtensionFilter;
      _fileSystemWatcher.Deleted += FileSystemChanged;
    }

    #endregion


    #region Events

    /// <summary>
    /// Occurs when settings file list has changed.
    /// </summary>
    public event EventHandler SettingsFileListChanged;

    /// <summary>
    /// Occurs when the settings auto update interval has changed.
    /// </summary>
    public event EventHandler SettingsAutoUpdateIntervalChanged;

    #endregion


    #region Properties

    /// <summary>
    /// Gets or sets the default settings file.
    /// </summary>
    /// <value>
    /// The default settings file.
    /// </value>
    [XmlExternalizable(false, SettingName = "DefaultSettingsFile")]
    [UsedImplicitly]
    public string DefaultSettingsFile
    {
      get;
      set;
    }


    /// <summary>
    /// Gets or sets the settings auto update interval.
    /// </summary>
    /// <value>
    /// The settings auto update interval.
    /// </value>
    [XmlExternalizable(5, SettingName = "SettingsAutoUpdateInterval")]
    [UsedImplicitly]
    public int SettingsAutoUpdateInterval
    {
      get
      {
        return _settingsAutoUpdateInterval;
      }

      set
      {
        if (_settingsAutoUpdateInterval == value)
        {
          return;
        }

        _settingsAutoUpdateInterval = value;

        if (SettingsAutoUpdateIntervalChanged != null)
        {
          SettingsAutoUpdateIntervalChanged(this, EventArgs.Empty);
        }
      }
    }


    /// <summary>
    /// Gets the scope that defines which store the data goes into.
    /// Must not be <c>Null</c>.
    /// </summary>
    /// <value>
    /// The scope that defines which store the data goes into.
    /// </value>
    public XmlExternalizationScope Scope
    {
      get
      {
        return XmlExternalizationScope.UserSettings;
      }
    }


    /// <summary>
    /// Gets or sets the settings folder path.
    /// </summary>
    /// <value>
    /// The settings folder path.
    /// </value>
    [XmlExternalizable("", SettingName = "SettingsFolderPath")]
    [UsedImplicitly]
    public string SettingsFolderPath
    {
      get
      {
        return _settingsFolderPath;
      }

      set
      {
        _settingsFolderPath = value;

        if (Directory.Exists(value))
        {
          _fileSystemWatcher.Path = value;
          _fileSystemWatcher.EnableRaisingEvents = true;
        }
        else
        {
          _fileSystemWatcher.EnableRaisingEvents = false;
        }

        GetSettingsFiles();
      }
    }


    /// <summary>
    /// Gets the Tag Name of the XML Element in the settings file.
    /// </summary>
    /// <value>
    /// The Tag Name of the XML Element in the settings file.
    /// </value>
    public string TagName
    {
      get
      {
        // tag name, should not conflict with any other plugins and internal ReSharper components
        return "SettingsManager.Settings";
      }
    }

    /// <summary>
    /// Gets or sets a value indicating whether to update settings automatically.
    /// </summary>
    /// <value>
    /// <c>True</c> if settings should be updated automatically; otherwise, <c>false</c>.
    /// </value>
    [XmlExternalizable(false, SettingName = "UpdateSettingsAutomatically")]
    [UsedImplicitly]
    public bool UpdateSettingsAutomatically
    {
      get;
      set;
    }

    /// <summary>
    /// Gets the instance.
    /// </summary>
    /// <value>
    /// The instance.
    /// </value>
    [NotNull]
    internal static ResharperSettingsManager Instance
    {
      get
      {
        return Shell.Instance.GetComponent<ResharperSettingsManager>();
      }
    }

    /// <summary>
    /// Gets the component settings managers.
    /// </summary>
    /// <value>
    /// The component settings managers.
    /// </value>
    [NotNull]
    internal IEnumerable<IComponentSettingsManager> ComponentSettingsManagers
    {
      get
      {
        return _componentSettingsManagers;
      }
    }

    /// <summary>
    /// Gets a value indicating whether the settings folder is valid.
    /// </summary>
    /// <value>
    /// <c>True</c> if the settings folder is valid; otherwise, <c>false</c>.
    /// </value>
    internal bool IsSettingsFolderValid
    {
      get
      {
        if (string.IsNullOrEmpty(SettingsFolderPath))
        {
          return false;
        }

        const int numRetries = 2;
        for (int i = 0; i <= numRetries; i++)
        {
          // do it several times, because netwok shares might timeout sometimes
          if (Directory.Exists(SettingsFolderPath))
          {
            return true;
          }

          // skip the last sleep
          if (i != numRetries)
          {
            Thread.Sleep(TimeSpan.FromSeconds(2));
          }
        }

        return false;
      }
    }

    /// <summary>
    /// Gets the collection of settings files.
    /// </summary>
    /// <value>
    /// The collection of settings files.
    /// </value>
    [NotNull]
    internal IEnumerable<SettingsFile> SettingsFiles
    {
      get
      {
        return new List<SettingsFile>(_settingsFiles);
      }
    }

    #endregion


    #region Public Methods

    /// <summary>
    /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
    /// </summary>
    public void Dispose()
    {
      _fileSystemWatcher.Dispose();
    }


    /// <summary>
    /// <para>
    /// Initializes the component. Called by the component container after the
    /// component is created.
    /// </para>
    /// <para>
    /// <see cref="M:System.IDisposable.Dispose"/> is the pairing method that's
    /// guaranteed to be called by the component container to tear down your
    /// component.
    /// </para>
    /// <para>
    /// You must not access other components before your 
    /// <see cref="M:JetBrains.ComponentModel.IComponent.Init"/> method is
    /// called.
    /// </para>
    /// <para>
    /// All of the components that you access from 
    /// <see cref="M:JetBrains.ComponentModel.IComponent.Init"/> are guaranteed
    /// to exist when your <see cref="M:System.IDisposable.Dispose"/> is called.
    /// Any other component might be missing by that time, and trying to get it
    /// will throw an exception.
    /// </para>
    /// </summary>
    public void Init()
    {
      var highlightingSettingsManager = new HighlightingSettingsManager();
      AddComponentSettingsManager(highlightingSettingsManager);

      var codeCleanupSettingsManager = new CodeCleanupSettingsManager();
      AddComponentSettingsManager(codeCleanupSettingsManager);

      var introduceStringConstantSettingsManager = new ResharperComponentSettingsManager(
        "AgentJohnson.IntroduceStringConstant", "Agent Johnson Introduce String Constant Settings");
      AddComponentSettingsManager(introduceStringConstantSettingsManager);

      var valueAnalysisSettingsManager = new ResharperComponentSettingsManager("AgentJohnson", "Agent Johnson Settings");
      AddComponentSettingsManager(valueAnalysisSettingsManager);

      var codeAnnotationsSettingsManager = new CodeAnnotationsSettingsManager();
      AddComponentSettingsManager(codeAnnotationsSettingsManager);

      var codeStyleSettingsManager = new CodeStyleSettingsManager();
      AddComponentSettingsManager(codeStyleSettingsManager);

      var todoItemsSettingsManager = new TodoItemsSettingsManager();
      AddComponentSettingsManager(todoItemsSettingsManager);

      var solutionSettingsManager = new ResharperComponentSettingsManager("Daemon.SolutionSettings", "Solution Files\\Folders Analysis Settings");
      AddComponentSettingsManager(solutionSettingsManager);

      var templatesSettingsManager = new TemplatesSettingsManager();
      AddComponentSettingsManager(templatesSettingsManager);

      var productsVersionsSettingsManager = new ProductsVersionsSettingsManager();
      AddComponentSettingsManager(productsVersionsSettingsManager);

      GetSettingsFiles();
    }


    /// <summary>
    /// <para>
    /// This method is called on the component to populate it with default or
    /// loaded settings, unless the component implements 
    /// <see cref="T:JetBrains.Util.IXmlUpgradable"/> and returns <c>True</c>
    /// from its handler.
    /// </para>
    /// <para>
    /// The element is taken from the settings file, if available.
    /// </para>
    /// <para>
    /// If not, and there are settings files from the older versions, and the
    /// component does not implement 
    /// <see cref="T:JetBrains.Util.IXmlUpgradable"/>, this method is called
    /// with the old settings.
    /// </para>
    /// <para>
    /// Otherwise, this method is called with an empty element or a <c>Null</c>
    /// value.
    /// </para>
    /// <para>
    /// For the first time, this method is called right before component's
    /// initialization.
    /// </para>
    /// <para>
    /// The settings-reading protocol might be executed more than once through
    /// the component lifetime.
    /// </para>
    /// </summary>
    /// <param name="element">
    /// An xml element to read settings from.
    /// </param>
    public void ReadFromXml(XmlElement element)
    {
      // read values via reflection
      XmlExternalizationUtil.ReadFromXml(element, this);
    }


    /// <summary>
    /// Called when the component should serialize its settings into the XML presentation, for saving into the settings file.
    /// </summary>
    /// <param name="element">
    /// An xml element to write settings to.
    /// </param>
    public void WriteToXml(XmlElement element)
    {
      // write values via reflection
      XmlExternalizationUtil.WriteToXml(element, this);
    }

    #endregion


    #region Methods

    /// <summary>
    /// Gets the settings files.
    /// </summary>
    internal void GetSettingsFiles()
    {
      _settingsFiles.Clear();

      if (string.IsNullOrEmpty(SettingsFolderPath) || !Directory.Exists(SettingsFolderPath))
      {
        if (SettingsFileListChanged != null)
        {
          SettingsFileListChanged(this, EventArgs.Empty);
        }

        return;
      }

      string[] filePaths = Directory.GetFiles(SettingsFolderPath, Resources.SettingsFileExtensionFilter);

      foreach (string filePath in filePaths)
      {
        var settingsFile = SettingsFile.Load(filePath);
        _settingsFiles.Add(settingsFile);
      }

      if (SettingsFileListChanged != null)
      {
        SettingsFileListChanged(this, EventArgs.Empty);
      }
    }


    /// <summary>
    /// Loads the settings from the specified Xml Node.
    /// </summary>
    /// <param name="resharperSettingsNode">The resharper settings node.</param>
    /// <param name="settingsManagers">The settings managers.</param>
    /// <param name="settingsManagersOptions">The settings managers options.</param>
    internal void LoadResharperSettings(
      [NotNull] XmlNode resharperSettingsNode,
      [CanBeNull] IEnumerable<IComponentSettingsManager> settingsManagers,
      [CanBeNull] IDictionary<string, object> settingsManagersOptions)
    {
      var elements = resharperSettingsNode.ChildNodes.Cast<XmlElement>().ToDictionary(element => element.Name);

      if (settingsManagers == null)
      {
        settingsManagers = _componentSettingsManagers;
      }

      if (settingsManagersOptions == null)
      {
        settingsManagersOptions = new Dictionary<string, object>();
      }

      var resharperComponentSettingsManagers = settingsManagers.OfType<ResharperComponentSettingsManager>();

      StopSolutionAnalysis();
      IEnumerable<IComponent> components = GetComponents();
      LoadResharperSettings(resharperComponentSettingsManagers, elements, components, settingsManagersOptions);

      IEnumerable<IComponent> solutionComponents = GetSolutionComponents();
      LoadResharperSettings(resharperComponentSettingsManagers, elements, solutionComponents, settingsManagersOptions);

      StartSolutionAnalysis();

      IEnumerable<CustomSettingsManager> customSettingsManagers = settingsManagers.OfType<CustomSettingsManager>();
      LoadCustomSettings(customSettingsManagers, settingsManagersOptions, elements);
    }


    /// <summary>
    /// Saves the settings.
    /// </summary>
    /// <param name="xmlDocument">The XML document.</param>
    /// <param name="settingsElement">The settings element.</param>
    /// <param name="componentSettingsManagers">The component settings managers.</param>
    /// <param name="settingsManagersOptions">The settings managers options.</param>
    internal void SaveResharperSettings(
      [NotNull] XmlDocument xmlDocument, 
      [NotNull] XmlElement settingsElement,
      [CanBeNull] IEnumerable<IComponentSettingsManager> componentSettingsManagers,
      [CanBeNull] IDictionary<string, object> settingsManagersOptions)
    {
      if (componentSettingsManagers == null)
      {
        componentSettingsManagers = _componentSettingsManagers;
      }

      foreach (IComponentSettingsManager manager in componentSettingsManagers)
      {
        XmlElement element = settingsElement[manager.ComponentTagName];

        if (element == null)
        {
          continue;
        }

        element.RemoveAll();
      }

      var resharperComponentSettingsManagers = componentSettingsManagers.OfType<ResharperComponentSettingsManager>();

      IEnumerable<IComponent> solutionComponents = GetSolutionComponents();
      SaveResharperSettings(xmlDocument, solutionComponents, resharperComponentSettingsManagers, settingsManagersOptions, settingsElement);

      IEnumerable<IComponent> collection = GetComponents();
      SaveResharperSettings(xmlDocument, collection, resharperComponentSettingsManagers, settingsManagersOptions, settingsElement);

      var customSettingsManagers = componentSettingsManagers.OfType<CustomSettingsManager>();
      SaveCustomSettings(customSettingsManagers, settingsManagersOptions, settingsElement);
    }


    /// <summary>
    /// Updates the settings.
    /// </summary>
    /// <param name="settingsNode">The settings node.</param>
    /// <param name="componentSettingsManagers">The component settings managers.</param>
    /// <param name="settingsManagersOptions">The settings managers options.</param>
    /// <returns>
    /// Returns <c>true</c>, if any settings were updated.
    /// </returns>
    internal bool UpdateResharperSettings(
      [NotNull] XmlNode settingsNode,
      [CanBeNull] IEnumerable<IComponentSettingsManager> componentSettingsManagers,
      [CanBeNull] IDictionary<string, object> settingsManagersOptions)
    {
      if (componentSettingsManagers == null)
      {
        componentSettingsManagers = _componentSettingsManagers;
      }

      List<IXmlExternalizableComponent> solutionComponents =
        GetSolutionComponents().Where(c => c is IXmlExternalizableComponent).Cast<IXmlExternalizableComponent>().ToList();

      List<IXmlExternalizableComponent> components =
        GetComponents().Where(c => c is IXmlExternalizableComponent).Cast<IXmlExternalizableComponent>().ToList();

      bool changed = false;

      var resharperComponentSettingsManagers = componentSettingsManagers.OfType<ResharperComponentSettingsManager>();

      foreach (XmlElement componentSettingsNode in settingsNode.ChildNodes)
      {
        string tagName = componentSettingsNode.Name;

        ResharperComponentSettingsManager manager =
          resharperComponentSettingsManagers.Where(m => m.ComponentTagName == tagName).FirstOrDefault();

        if (manager == null)
        {
          continue;
        }

        var component = solutionComponents.FirstOrDefault(c => c.TagName == manager.ComponentTagName)
                        ?? components.FirstOrDefault(c => c.TagName == manager.ComponentTagName);

        if (component == null)
        {
          continue;
        }

        object options = GetSettingsManagerOptions(settingsManagersOptions, component.TagName);

        changed |= manager.UpdateSettings(component, componentSettingsNode, options);
      }

      // todo: add custom settings managers support.
      return changed;
    }


    [NotNull]
    private static IEnumerable<IComponent> GetComponents()
    {
      return Shell.Instance.GetInitializedComponentsInStableOrder();
    }


    [NotNull]
    private static IEnumerable<IComponent> GetSolutionComponents()
    {
      if (SolutionManager.Instance == null || SolutionManager.Instance.CurrentSolution == null)
      {
        return new List<IComponent>();
      }

      var solutionBase = SolutionManager.Instance.CurrentSolution as SolutionBase;

      return solutionBase == null ? new List<IComponent>() : solutionBase.GetInitializedComponentsInStableOrder();
    }


    private static void LoadResharperSettings(
      [NotNull] IEnumerable<ResharperComponentSettingsManager> settingsManagers,
      [NotNull] IDictionary<string, XmlElement> settingsXmlElements,
      [NotNull] IEnumerable<IComponent> components,
      [NotNull] IDictionary<string, object> settingsManagersOptions)
    {
      foreach (IComponent component in components)
      {
        var externalizable = component as IXmlExternalizableComponent;

        if (externalizable == null)
        {
          continue;
        }

        if (!settingsXmlElements.ContainsKey(externalizable.TagName))
        {
          continue;
        }

        ResharperComponentSettingsManager componentSettingsManager =
          settingsManagers.Where(m => m.ComponentTagName == externalizable.TagName).FirstOrDefault();

        if (componentSettingsManager == null)
        {
          continue;
        }

        object options = GetSettingsManagerOptions(settingsManagersOptions, externalizable.TagName);

        XmlElement element = settingsXmlElements[externalizable.TagName];
        componentSettingsManager.LoadSettings(externalizable, element, options);
      }
    }


    private static void LoadCustomSettings(
      IEnumerable<CustomSettingsManager> customSettingsManagers,
      IDictionary<string, object> settingsManagersOptions,
      IDictionary<string, XmlElement> elements)
    {
      foreach (CustomSettingsManager settingsManager in customSettingsManagers)
      {
        if (!elements.ContainsKey(settingsManager.ComponentTagName))
        {
          continue;
        }

        XmlElement xmlElement = elements[settingsManager.ComponentTagName];

        object options = GetSettingsManagerOptions(settingsManagersOptions, settingsManager.ComponentTagName);

        settingsManager.LoadSettings(xmlElement, options);
      }
    }


    [CanBeNull]
    private static object GetSettingsManagerOptions(
      [CanBeNull] IDictionary<string, object> settingsManagersOptions, string componentTagName)
    {
      object options = null;

      if (settingsManagersOptions != null && settingsManagersOptions.ContainsKey(componentTagName))
      {
        options = settingsManagersOptions[componentTagName];
      }

      return options;
    }


    private static void SaveResharperSettings(
      [NotNull] XmlDocument xmlDocument, 
      [NotNull] IEnumerable<IComponent> components, 
      [NotNull] IEnumerable<ResharperComponentSettingsManager> componentSettingsManagers, 
      [CanBeNull] IDictionary<string, object> settingsManagersOptions,
      [NotNull] XmlNode settingsElement)
    {
      foreach (IComponent component in components)
      {
        var externalizable = component as IXmlExternalizableComponent;

        if (externalizable == null)
        {
          continue;
        }

        ResharperComponentSettingsManager settingsManager =
          componentSettingsManagers.Where(m => m.ComponentTagName == externalizable.TagName).FirstOrDefault();

        if (settingsManager == null)
        {
          continue;
        }

        XmlElement element = settingsElement[externalizable.TagName];

        if (element == null)
        {
          element = xmlDocument.CreateElement(externalizable.TagName);
          settingsElement.AppendChild(element);
        }

        if (element.Attributes.Count != 0 || element.ChildNodes.Count != 0)
        {
          // was already saved
          continue;
        }

        object options = GetSettingsManagerOptions(settingsManagersOptions, externalizable.TagName);

        settingsManager.SaveSettings(externalizable, element, options);
      }
    }


    private static void SaveCustomSettings(
      [NotNull] IEnumerable<CustomSettingsManager> customSettingsManagers,
      [CanBeNull] IDictionary<string, object> settingsManagersOptions,
      [NotNull] XmlNode settingsElement)
    {
      foreach (CustomSettingsManager settingsManager in customSettingsManagers)
      {
        XmlElement element = settingsElement[settingsManager.ComponentTagName]
                             ?? settingsElement.CreateElement(settingsManager.ComponentTagName);

        object options = GetSettingsManagerOptions(settingsManagersOptions, settingsManager.ComponentTagName);

        settingsManager.SaveSettings(element, options);
      }
    }


    private static void StartSolutionAnalysis()
    {
      if (SolutionManager.Instance == null || SolutionManager.Instance.CurrentSolution == null)
      {
        return;
      }

      Daemon.GetInstance(SolutionManager.Instance.CurrentSolution).Suspended = false;
      Daemon.GetInstance(SolutionManager.Instance.CurrentSolution).Invalidate();
    }


    private static void StopSolutionAnalysis()
    {
      if (SolutionManager.Instance != null && SolutionManager.Instance.CurrentSolution != null)
      {
        Daemon.GetInstance(SolutionManager.Instance.CurrentSolution).Suspended = true;
      }
    }


    private void AddComponentSettingsManager([NotNull] IComponentSettingsManager highlightingComponentSettingsManager)
    {
      _componentSettingsManagers.Add(highlightingComponentSettingsManager);
    }


    private void FileSystemChanged([NotNull] object sender, [NotNull] FileSystemEventArgs e)
    {
      GetSettingsFiles();
    }

    #endregion
  }
}